import { DiffLine, DiffType } from "../index.js"

import { LineStream, matchLine } from "./util.js"

/**
 * https://blog.jcoglan.com/2017/02/12/the-myers-diff-algorithm-part-1/
 * Invariants:
 * - new + same = newLines.length
 * - old + same = oldLinesCopy.length
 * ^ (above two guarantee that all lines get represented)
 * - Lines are always output in order, at least among old and new separately
 * - Old lines in a hunk are always output before the new lines
 */
export async function* streamDiff(oldLines: string[], newLines: LineStream): AsyncGenerator<DiffLine> {
	const oldLinesCopy = [...oldLines]

	// If one indentation mistake is made, others are likely. So we are more permissive about matching
	let seenIndentationMistake = false

	let newLineResult = await newLines.next()

	while (oldLinesCopy.length > 0 && !newLineResult.done) {
		const { matchIndex, isPerfectMatch, newLine } = matchLine(
			newLineResult.value,
			oldLinesCopy,
			seenIndentationMistake,
		)

		if (!seenIndentationMistake && newLineResult.value !== newLine) {
			seenIndentationMistake = true
		}

		let type: DiffType

		const isNewLine = matchIndex === -1

		if (isNewLine) {
			type = "new"
		} else {
			// Insert all deleted lines before match
			for (let i = 0; i < matchIndex; i++) {
				yield { type: "old", line: oldLinesCopy.shift()! }
			}
			type = isPerfectMatch ? "same" : "old"
		}

		switch (type) {
			case "new":
				yield { type, line: newLine }
				break

			case "same":
				yield { type, line: oldLinesCopy.shift()! }
				break

			case "old":
				yield { type, line: oldLinesCopy.shift()! }
				yield { type: "new", line: newLine }
				break

			default:
				console.error(`Error streaming diff, unrecognized diff type: ${type}`)
		}
		newLineResult = await newLines.next()
	}

	// Once at the edge, only one choice
	if (newLineResult.done && oldLinesCopy.length > 0) {
		for (const oldLine of oldLinesCopy) {
			yield { type: "old", line: oldLine }
		}
	}

	if (!newLineResult.done && oldLinesCopy.length === 0) {
		yield { type: "new", line: newLineResult.value }
		for await (const newLine of newLines) {
			yield { type: "new", line: newLine }
		}
	}
}
